import fg from "fast-glob";
import fs from "fs-extra";
import { default as ssim } from "image-ssim";
import path from "path";
import sizeOf from "image-size";
import { CmdOption } from "../typings/index.js";
import { UnityHelper } from "./UnityHelper.js";
import { loadImage } from "./loadImage.js";
import { Status } from "../tools/Status.js";
import { toolchain } from "../toolchain.js";

interface ImageInfo {
    file: string,
    img: ssim.IImage | null,
    similarTo?: string
}

interface DelInfo {
    reservedFile: string,
    deleteFiles: string[],
    reservedGUID: string,
    deleteGUIDs: string[]
}

export class ImageDedup {
    private readonly dupGuideText = `########## INSTRUCMENT ##########
# 正如现在所看到的，以#号开头均为注释
#
# 本日志由命令\`unity-find-fault -p {PROJECT} -a dedupImage -o {OUTPUT}\`生成
# 可使用命令\`unity-find-fault -p {PROJECT} -a dedupImage --delete-instruction {OUTPUT}\`执行资源删除
#
# 执行删除命令前，请先详细查看每组资源确定是否应当删除，如不能删除（比如代码中会动态加载），则将其从本日志中删去
# 编辑本日志时注意保持格式
#
# +号开头表示保留该资源，-号开头表示该资源将被替换并删除
# +/-后括号内的数字表示该资源的静态引用次数，如果整组资源的静态引用次数均为0，则整组全部删除，不论是否以+号开头
#
# 通常来说，模型贴图如果静态引用次数为0，都是可以删除的
# 但需注意某些资源虽然静态引用次数为0，但可能存在被代码动态加载的情况，比如:
# Assets/Arts/platIcon下的Icon
# Assets/AssetSources/icon下的图标
# Assets/AssetSources/ui/atlas下的某些图集文件（通常以数字有规律地命名）
########## ########### ##########`;

private readonly diffGuideText = `########## INSTRUCMENT ##########
# 以下图片均为图像相同但import类型设置不同的图片，比如图片A是Sprite而图片B是Texture，需要程序修改使用方式后才可进行替换删除
########## ########### ##########`;

private readonly iconGuideText = `########## INSTRUCMENT ##########
# 以下图片均为程序根据表格配置读取的重复图标，请策划修改表格并删除其中重复的图片
########## ########### ##########`;

private readonly mixIconGuideText = `########## INSTRUCMENT ##########
# 以下图片均为程序根据表格配置读取的重复图标，这些图标分别存在于不同的文件夹中，需要程序修改统一使用方式后才可进行替换删除
# 推荐方案是保留icon图标，修改使用其他图标的地方
########## ########### ##########`;

private readonly unknownGuideText = `########## INSTRUCMENT ##########
# 以下图片均为程序无法辨识的图片，请人工甄别是否重复
########## ########### ##########`;

    private groupMap: { [key: string]: ImageInfo[] } = {};

    public async execute(): Promise<void> {
        if (toolchain.opts.deleteInstruction == null) {
            // 不删除，只输出重复图片信息
            await this.exportDuplicated();
        } else {
            await this.replaceByInstruction();
        }
    }

    private async exportDuplicated(): Promise<void> {
        if (toolchain.opts.output == null) {
            console.error('No output specified.');
            return;
        }

        // 确定icon路径
        const iconPath = path.join(toolchain.opts.projectRoot, 'Assets/AssetSources/icon');
        if (!fs.existsSync(iconPath)) {
            console.error('Cannot reach icon path!');
            return;
        }

        const roots = toolchain.opts.input ? toolchain.opts.input.split(',').map(v => path.join(toolchain.opts.projectRoot, v)) : [path.join(toolchain.opts.projectRoot, 'Assets')];
        // 目前只能处理png
        for (const root of roots) {
            const imgs = await fg(['**/*.png', '**/*.jpg', '**/*.tga'], { cwd: root });
            // 先按照size/width/height归类
            for (const img of imgs) {
                const file = path.join(root, img);
                try {
                    const dimensions = await sizeOf(file);
                    const fstat = await fs.stat(file);
                    const key = `${path.extname(img).toLowerCase()}_${fstat.size}_${dimensions.width}_${dimensions.height}`;
                    let group = this.groupMap[key];
                    if (group == null) {
                        this.groupMap[key] = group = [];
                    }
                    group.push({ file, img: null });
                } catch(e) {
                    console.error('get dimesion failed:', file);
                }
            }
        }

        // 比较
        Status.Instance.startTask('Compare images', Object.keys(this.groupMap).length);

        const duplicates: string[][] = [];
        const diffSettings: string[][] = [];
        const allIcons: string[][] = [];
        const mixIcons: string[][] = [];
        const unkowns: string[][] = [];
        for (const key in this.groupMap) {
            Status.Instance.update(key);

            const group = this.groupMap[key];
            const cnt = group.length;
            if (cnt == 1) continue;

            for (const info of group) {
                const img = await loadImage(info.file);
                info.img = img;
            }

            // 检查如果是不受支持的图片类型，则列入suspected
            if (group[0].img == null) {
                unkowns.push(group.map((v) => v.file));
                continue;
            }

            const dupMap: { [file: string]: string[] } = {};
            const diffSettingMap: { [file: string]: string[] } = {};
            for (let i = 0; i < cnt - 1; i++) {
                const infoA = group[i];
                if (infoA.similarTo) continue;
                // const metaA = await UnityHelper.loadMeta<Unity.Meta.Texture>(infoA.file + '.meta');
                const metaA = await UnityHelper.readTextureMeta(infoA.file + '.meta');
                for (let j = i + 1; j < cnt; j++) {
                    const infoB = group[j];
                    if (infoB.similarTo) continue;
                    // 再比较图片相似度
                    if (infoA.img != null && infoB.img != null) {
                        const rst = ssim.compare(infoA.img, infoB.img);
                        // console.log(infoA.file, infoB.file, rst);
                        if (rst.ssim == 1) {
                            // 再比较图片设置
                            // const metaB = await UnityHelper.loadMeta<Unity.Meta.Texture>(infoB.file + '.meta');
                            const metaB = await UnityHelper.readTextureMeta(infoB.file + '.meta');
                            // if (metaA.TextureImporter?.textureType != metaB.TextureImporter?.textureType) {
                            if (metaA.textureType != metaB.textureType || 
                                metaA.spriteBorder.x != metaB.spriteBorder.x || 
                                metaA.spriteBorder.y != metaB.spriteBorder.y || 
                                metaA.spriteBorder.z != metaB.spriteBorder.z || 
                                metaA.spriteBorder.w != metaB.spriteBorder.w) {
                                // 先比较texture type
                                let diffs = diffSettingMap[infoA.file];
                                if (diffs == null) {
                                    diffSettingMap[infoA.file] = diffs = [];
                                }
                                diffs.push(infoB.file);
                                continue;
                            }
                            let dups = dupMap[infoA.file];
                            if (dups == null) {
                                dupMap[infoA.file] = dups = [];
                            }
                            dups.push(infoB.file);
                            infoB.similarTo = infoA.file;
                        }
                    }
                }
            }
            for (const file in dupMap) {
                const dups: string[] = dupMap[file];
                dups.unshift(file);
                // 判断分类
                let iconCnt = 0;
                for (const d of dups) {
                    if (d.includes(iconPath)) iconCnt++;
                }
                const dcnt = dups.length;
                if (iconCnt == dcnt) {
                    allIcons.push(dups);
                } else if (iconCnt > 0) {
                    mixIcons.push(dups);
                } else {
                    duplicates.push(dups);
                }
            }
            for (const file in diffSettingMap) {
                const diffs: string[] = diffSettingMap[file];
                diffs.unshift(file);
                duplicates.push(diffs);
            }
            // 卸载所有图片数据防止内存撑爆
            group.forEach((v) => v.img = null);
        }

        Status.Instance.end();

        // 输出重复图片信息
        await this.dump(duplicates, this.dupGuideText, toolchain.opts.output);
        
        // 输出设置不同的重复图片信息
        await this.dump(diffSettings, this.diffGuideText, this.getOutput(toolchain.opts.output, 'diffSetting'));

        // 输出重复图标信息
        await this.dump(allIcons, this.iconGuideText, this.getOutput(toolchain.opts.output, 'icon'));

        // 输出混合图标信息
        await this.dump(mixIcons, this.mixIconGuideText, this.getOutput(toolchain.opts.output, '.mixIcon'));

        // 目前只支持png/jpg/tga，输出其他不受支持的图片类型
        await this.dump(unkowns, this.unknownGuideText, this.getOutput(toolchain.opts.output, 'unknown'));
    }

    private async dump(duplicates: string[][], guideText: string, output: string): Promise<void> {
        const allFiles = duplicates.flat();
        // 按引用次数排序
        const refMap = await this.countRef(allFiles);
        for (const dups of duplicates) {
            dups.sort((a, b) => refMap[b] - refMap[a]);
            // console.log('------------');
            // dups.forEach((value, index) => console.log(`${index == 0 ? '+' : '-'}(${refMap[value]}) file:///${value}`));
        }
        if (output != null) {
            let content = guideText.replaceAll('{PROJECT}', toolchain.opts.projectRoot).replaceAll('{OUTPUT}', toolchain.opts.output!);
            for (const dups of duplicates) {
                content += '------------\n';
                dups.forEach((value, index) => content += `${index == 0 ? '+' : '-'}(${refMap[value]}) file:///${value}\n`);
            }
            await fs.ensureFile(output);
            await fs.writeFile(output, content, 'utf-8');
        }
    }

    private async countRef(files: string[]): Promise<{ [file: string]: number }> {
        const refMap: { [file: string]: number } = {};
        const guidMap: { [file: string]: string } = {};
        for (const file of files) {
            guidMap[file] = await UnityHelper.readGUID(file + '.meta');
            refMap[file] = 0;
        }

        // 检查所有prefab和material
        const checkRoot = path.join(toolchain.opts.projectRoot, 'Assets');
        const checkedFiles = await fg(['**/*.prefab', '**/*.mat'], { cwd: checkRoot });
        for (const f of checkedFiles) {
            const file = path.join(checkRoot, f);
            const content = await fs.readFile(file, 'utf-8');
        
            for (const file of files) {
                const results = content.matchAll(new RegExp(guidMap[file], 'g'));
                for (const rst of results) {
                    refMap[file] = refMap[file] + 1;
                }
            }
        }
        return refMap;
    }

    private async replaceByInstruction(): Promise<void> {
        // 将需要删除的图片挑选出来存入指定txt中并读取删除
        const dinfos = await this.readDeleteInstruction(toolchain.opts.deleteInstruction!);

        // 检查所有prefab和material
        const checkRoot = path.join(toolchain.opts.projectRoot, 'Assets');
        const checkedFiles = await fg(['**/*.prefab', '**/*.mat'], { cwd: checkRoot });
        for (const f of checkedFiles) {
            const file = path.join(checkRoot, f);
            const content = await fs.readFile(file, 'utf-8');
            let newContent = content;
            for (const dinfo of dinfos) {
                if (!dinfo.reservedGUID) continue;
                for (const td of dinfo.deleteGUIDs) {
                    newContent = newContent.replaceAll(td, dinfo.reservedGUID);
                }
            }
            if (newContent != content) {
                console.log('file modified:', file);
                await fs.writeFile(file, newContent, 'utf-8');
            }
        }

        for (const dinfo of dinfos) {
            for (const td of dinfo.deleteFiles) {
                await fs.unlink(td);
                await fs.unlink(td + '.meta');
            }
        }
    }

    private async readDeleteInstruction(instFile: string): Promise<DelInfo[]> {
        const dicontent = await fs.readFile(instFile, 'utf-8');
        const lines = dicontent.split(/\r?\n/);
        const dinfos: DelInfo[] = [];
        let dinfo: DelInfo | null = null;
        for (const line of lines) {
            if (line.startsWith('#')) continue;
            if (line.startsWith('---')) {
                dinfo = null;
            } else {
                const f = line.replace(/[\+\-]\(\d+\) file:\/\/\//, '');
                if (fs.existsSync(f)) {
                    if (dinfo == null) {
                        dinfo = { reservedFile: '', deleteFiles: [], reservedGUID: '', deleteGUIDs: [] };
                        dinfos.push(dinfo);
                    }
                    const guid = await UnityHelper.readGUID(f + '.meta');
                    if (line.startsWith('+')) {
                        dinfo.reservedFile = f;
                        dinfo.reservedGUID = guid;
                    } else {
                        dinfo.deleteFiles.push(f);
                        dinfo.deleteGUIDs.push(guid)
                    }
                }
            }
        }
        return dinfos;
    }

    private getOutput(original: string, desc: string): string {
        const outExt = path.extname(original);
        return outExt != '' ? original.replace(outExt, `.${desc}` + outExt) : original + `.${desc}`;
    }
}
